// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.core.ws.runtime.xml;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import com.microsoft.tfs.core.ws.runtime.Schemas;
import com.microsoft.tfs.util.Check;

/**
 * <p>
 * Static helper methods for XML deserialization via StAX. These methods will be
 * called from beans generated by com.microsoft.tfs.core.ws.generator.
 * </p>
 *
 *
 * @threadsafety thread-compatible
 */
public abstract class XMLStreamReaderHelper {
    /**
     * Advances the {@link XMLStreamReader} until it has read the end of the
     * current element. Useful when an element is encountered while reading a
     * stream, and it should be skipped.
     *
     * @param reader
     *        the stream reader to read from (not null).
     */
    public final static void readUntilElementEnd(final XMLStreamReader reader) throws XMLStreamException {
        int event = reader.getEventType();

        Check.isTrue(event == XMLStreamConstants.START_ELEMENT, "event == XMLStreamConstants.START_ELEMENT"); //$NON-NLS-1$

        /*
         * Start element depth at 1, increment when an element is started (not
         * including the element that we start with), decrement when an element
         * is ended, and when it goes to 0 we've read the end of the original
         * reader's element.
         */
        int elementDepth = 1;

        boolean firstTime = true;
        while (true) {
            switch (event) {
                case XMLStreamConstants.START_ELEMENT:
                    /*
                     * Don't increment depth the first time through, because the
                     * caller opened the element.
                     */
                    if (firstTime) {
                        firstTime = false;
                    } else {
                        elementDepth++;
                    }

                    break;
                case XMLStreamConstants.END_ELEMENT:
                    elementDepth--;

                    if (elementDepth < 1) {
                        /*
                         * We just wrote the end element for the original
                         * element.
                         */
                        return;
                    }

                    break;
                default:
                    /*
                     * Things like characters, comments, attributes, etc. Ignore
                     * them all.
                     */
            }

            event = reader.next();
        }
    }

    /**
     * Reads an {@link Object} from the current element in the given reader,
     * using the type specified by the XML Schema Instance "type" attribute.
     * Supports most Java object types which map to XSD simple types (Integer,
     * Short, Byte, Boolealn, Calendar, etc.), and also arrays of those types,
     * and arrays of arrays, etc.
     * <p>
     * Not all distinct serializable types are supported. For example, only XML
     * Schema "dateTime" is deserialized as a {@link Calendar}. The XML Schema
     * "date" is not supported. Visual Studio has the same behavior.
     *
     * @param reader
     *        the reader (not null)
     * @return the {@link Object}
     * @throws XMLStreamException
     */
    public final static Object readObjectElement(final XMLStreamReader reader) throws XMLStreamException {
        String typeString = reader.getAttributeValue(Schemas.XSI, "type"); //$NON-NLS-1$

        if (typeString != null && typeString.length() > 0) {
            /*
             * Discard any prefix if the type has one ("xsd:integer"), since all
             * the local names are unique as far as this method is concerned.
             */
            final String[] components = typeString.split(":"); //$NON-NLS-1$
            if (components.length == 2) {
                typeString = components[1];
            }

            /*
             * The data is stored in the element text. Keep these tests
             * alphabetical for maintenance.
             */
            if (typeString.equals("base64Binary")) //$NON-NLS-1$
            {
                return XMLConvert.toByteArray(reader.getElementText());
            } else if (typeString.equals("boolean")) //$NON-NLS-1$
            {
                return Boolean.valueOf(XMLConvert.toBoolean(reader.getElementText()));
            } else if (typeString.equals("char")) //$NON-NLS-1$
            {
                return new Character(XMLConvert.toCharacter(reader.getElementText()));
            } else if (typeString.equals("date")) //$NON-NLS-1$
            {
                return XMLConvert.toCalendar(reader.getElementText(), false);
            } else if (typeString.equals("dateTime")) //$NON-NLS-1$
            {
                return XMLConvert.toCalendar(reader.getElementText(), true);
            } else if (typeString.equals("decimal")) //$NON-NLS-1$
            {
                return XMLConvert.toBigDecimal(reader.getElementText());
            } else if (typeString.equals("double")) //$NON-NLS-1$
            {
                return new Double(XMLConvert.toDouble(reader.getElementText()));
            } else if (typeString.equals("float")) //$NON-NLS-1$
            {
                return new Float(XMLConvert.toFloat(reader.getElementText()));
            } else if (typeString.equals("int")) //$NON-NLS-1$
            {
                return new Integer(XMLConvert.toInt(reader.getElementText()));
            } else if (typeString.equals("guid")) //$NON-NLS-1$
            {
                return XMLConvert.toGUID(reader.getElementText());
            } else if (typeString.equals("long")) //$NON-NLS-1$
            {
                return new Long(XMLConvert.toLong(reader.getElementText()));
            } else if (typeString.equals("short")) //$NON-NLS-1$
            {
                return new Short(XMLConvert.toShort(reader.getElementText()));
            } else if (typeString.equals("string")) //$NON-NLS-1$
            {
                /*
                 * These streams are always coalescing, so we get the entire
                 * element text.
                 */
                return reader.getElementText();
            } else if (typeString.equals("unsignedByte")) //$NON-NLS-1$
            {
                /*
                 * Java has no unsigned byte type, but programmers are familiar
                 * with the signed byte, so return one of those instead. A
                 * simple cast handles converting too-large short values into
                 * negative byte values.
                 */
                return new Byte((byte) XMLConvert.toShort(reader.getElementText()));
            } else if (typeString.equals("ArrayOfAnyType")) //$NON-NLS-1$
            {
                return readObjectArray(reader);
            }

            /*
             * Don't know how to handle this element, just read until it's over.
             */
            readUntilElementEnd(reader);
        } else {
            /*
             * Element with no type string it may be an XSI nil marker.
             */

            final String nilAttribute = reader.getAttributeValue(Schemas.XSI, "nil"); //$NON-NLS-1$

            if (nilAttribute != null && nilAttribute.equals("true")) //$NON-NLS-1$
            {
                /*
                 * No contents, read until the end of the element.
                 */
                readUntilElementEnd(reader);
                return null;
            }
        }

        /*
         * Unknown kind of element, no object type available.
         */
        return null;
    }

    /**
     * Reads an array of {@link Object}s from the stream, possibly recursively
     * calling this method or {@link #readObjectElement(XMLStreamReader)}.
     *
     * @param reader
     *        the reader (not null).
     * @throws XMLStreamException
     */
    private static Object[] readObjectArray(final XMLStreamReader reader) throws XMLStreamException {
        final List ret = new ArrayList();

        int event;
        do {
            event = reader.next();

            if (event == XMLStreamConstants.START_ELEMENT) {
                final String nilAttribute = reader.getAttributeValue(Schemas.XSI, "nil"); //$NON-NLS-1$

                if (nilAttribute != null && nilAttribute.equals("true")) //$NON-NLS-1$
                {
                    ret.add(null);
                } else {
                    ret.add(readObjectElement(reader));
                }
            }
        } while (event != XMLStreamConstants.END_ELEMENT);

        return ret.toArray(new Object[ret.size()]);
    }
}
